/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

const LOCAL_ERR_ILLEGAL_CONSTRUCTOR: typeof ERR_ILLEGAL_CONSTRUCTOR =
    Reflect.get(globalThis, 'ERR_ILLEGAL_CONSTRUCTOR');
const localSignalMaps = signalMaps;
const signalName = new Map<number, string>();
for (const k of Object.keys(localSignalMaps)) {
    signalName.set(localSignalMaps[k], k);
}
const localCreateStdioReadable: typeof createStdioReadable = Reflect.get(globalThis, "createStdioReadable");

declare type InnerSpawnOptions = [
    string,                  // file
    string[],                // args
    string[],                // env
    string,                  // cwd
    boolean,                 // detach
    StdioType | StdioType[], // stdio
    number,                  // uid
    number,                  // gid
]

const innerSpawn: (that: ChildProcess, option: InnerSpawnOptions,
    exitCallback: (exitCode: number, signal: number) => void) => [number, [bigint, bigint, bigint]] =
    exchangeInnerMethod(globalThis, "_innerSpawn");
const innerReadStream = exchangeInnerMethod(globalThis, "_innerReadStream") as (stream: bigint, onread: (buffer: ArrayBuffer) => void) => void;
const innerGetFd = exchangeInnerMethod(globalThis, "_innerGetFd") as (stream: bigint) => number;

interface ChildProcessStdioStorage {
    fd: (bigint | null)[];
    stdin?: BinaryFile;
    stdout?: Readable<Uint8Array>;
    stderr?: Readable<Uint8Array>;
}


const innerChildProcessStdio: WeakMap<ChildProcess, ChildProcessStdioStorage> = new WeakMap();

class ChildProcess extends EventTarget {
    constructor() {
        throw new LOCAL_ERR_ILLEGAL_CONSTRUCTOR('ChildProcess');
        super();
    }

    stdio: ChildProcessStdio;
}

const readStream = (data: StdioReadableStorage<any>) => {
    if (data.data !== undefined) {
        return;
    }
    data.data = [];
    innerReadStream(data.fd as bigint, (buffer: ArrayBuffer) => {
        data.controller.enqueue(new Uint8Array(buffer));
    });
}

const createChildProces = (): ChildProcess => {
    const childProcess = new EventTarget() as ChildProcess;
    Reflect.setPrototypeOf(childProcess, ChildProcess.prototype);
    Reflect.set(childProcess, 'stdio',
        {
            get stdin(): BinaryFile | undefined {
                const ios = innerChildProcessStdio.get(childProcess)!;
                if (ios.stdin === undefined && ios.fd[0] !== null) {
                    ios.stdin = new File(innerGetFd(ios.fd[0]), 'wb') as BinaryFile;
                }
                return ios.stdin;
            },

            get stdout(): Readable<Uint8Array> | undefined {
                const ios = innerChildProcessStdio.get(childProcess)!;
                if (ios.stdout === undefined && ios.fd[1] !== null) {
                    ios.stdout = localCreateStdioReadable(ios.fd[1], readStream);
                }
                return ios.stdout;
            },

            get stderr(): Readable<Uint8Array> | undefined {
                const ios = innerChildProcessStdio.get(childProcess)!;
                if (ios.stderr === undefined && ios.fd[2] !== null) {
                    ios.stderr = localCreateStdioReadable(ios.fd[2], readStream);
                }
                return ios.stderr;
            }
        });
    return childProcess as ChildProcess;
}

namespace child_process {
    export function spawn(command: string, args: string[], option?: SpawnOption): ChildProcess {
        const childProcess = createChildProces();
        let file: string = command;
        const preArgv: string[] = [];
        if (option?.shell !== false && option?.shell !== undefined) {
            if (option?.shell === true) {
                file = '/bin/sh';
            } else {
                file = option.shell;
            }
            preArgv.push(option?.argv0 ?? file);
            preArgv.push(command);
        } else {
            preArgv.push(option?.argv0 ?? command);
        }
        const env = option?.env ?? process.env.toString().split('\n');
        let stdio: StdioType[] = [];
        if (option?.stdio instanceof Array) {
            stdio.push(...option.stdio);
        } else {
            switch (option?.stdio) {
                case 'pipe':
                case 'overlapped':
                case 'ignore':
                case 'inherit':
                    for (let _ = 0; _ < 3; _++) {
                        stdio.push(option.stdio);
                    }
                    break;
                default:
                    for (let _ = 0; _ < 3; _++) {
                        stdio.push('inherit');
                    }
                    break;
            }
        }
        const innerOptions: InnerSpawnOptions = [
            file,
            [...preArgv, ...args],
            env,
            option?.cwd ?? process.cwd(),
            option?.detached ?? false,
            stdio,
            option?.uid ?? process.getuid(),
            option?.gid ?? process.getgid(),
        ];
        const results = innerSpawn(childProcess, innerOptions, (status: number, signal: number) => {
            const s = signal === 0 ? null : signalName.get(signal)!;
            const e = signal !== 0 ? null : status;

            const exitEv = new Event('exit') as ChildProcessExitEvent;
            const closeEv = new Event('close') as ChildProcessExitEvent;
            const errorEv = new Event('error') as ChildProcessErrorEvent;
            exitEv.code = e;
            exitEv.signal = s;
            closeEv.code = e;
            closeEv.signal = s;
            errorEv.err = new Error(`subprocess is exit caused by signal: ${s}`);
            childProcess.dispatchEvent(closeEv);
            childProcess.dispatchEvent(exitEv);
            if (s !== null)
                childProcess.dispatchEvent(errorEv);
        });
        if (results[0] === 0) {
            childProcess.dispatchEvent(new Event('spawn'));
        } else {
            const ev = new Event('error') as ChildProcessErrorEvent;
            ev.err = new Error(`failed to spawn subprocess, code: ${results[0]}`);
            queueMicrotask(() => {
                childProcess.dispatchEvent(ev);
            });
        }
        {
            const fds: (bigint | null)[] = results[1];
            stdio.forEach((type, index) => {
                if (typeof fds[index] === 'number') {
                    fds[index] = null;
                    return;
                }
                switch (type) {
                    case 'ignore':
                    case 'inherit':
                        fds[index] = null;
                        break;
                }
            });
            innerChildProcessStdio.set(childProcess, {
                fd: fds,
            });
        }
        return childProcess;
    };
}

exportToObject(globalThis, 'child_process', child_process);
